package cn.lcvc.xnc.application.common.exception.handler;


import cn.lcvc.xnc.application.common.model.result.ResponseData;
import cn.lcvc.xnc.application.common.model.result.ResultCode;
import cn.lcvc.xnc.application.common.exception.model.MyExcelException;
import cn.lcvc.xnc.application.common.exception.model.MyServiceException;
import cn.lcvc.xnc.application.common.exception.model.MyTokenException;
import cn.lcvc.xnc.application.common.exception.model.MyWebException;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.*;

/**
 * @author ljy
 * 20190819创建
 * 20220115最后修改
 * 用于处理本服务端所有异常信息并返回前端
 * 说明：使用此异常类，应按照把业务层异常往控制层抛，并且在控制层不轻易做异常处理，都交由此类处理
 */
@Slf4j
@ControllerAdvice
@ResponseBody
@Order(100)//如果其他module有异常处理类，则应设计在100以下的数值
public class MyExceptionHandler {

    /**
     * 处理自定义异常：表单/前端异常
     */
    @ExceptionHandler
    public ResponseData MyWebException(MyWebException e) {
        log.info("前端异常", e.getMessage());
        return ResponseData.error(e.getMessage());
    }

    /**
     * 处理自定义异常：处理业务异常
     */
    @ExceptionHandler
    public ResponseData myServiceException(MyServiceException e) {
        log.info("业务异常", e.getMessage());
        return ResponseData.error(e.getMessage());
    }

    /**
     * 处理自定义表格导入异常
     * 说明：返回的异常信息为集合类的json字符串。需要重新将json转换为对象集合再传递到前端
     */
    @ExceptionHandler
    public ResponseData myServiceException(MyExcelException e) {
        log.info("表格导入异常", e.getMessage());
        return ResponseData.error("表格导入失败",JSON.parse(e.getMessage()));
    }

    /**
     * 处理自定义异常：token异常
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler
    public ResponseData myTokenException(MyTokenException e) {
        log.info("token异常", e.getMessage());
        return ResponseData.error(ResultCode.UNAUTHORIZED.getCode(),e.getMessage());
    }

    /**
     * 处理权限异常
     */
    /*@ExceptionHandler
    public ResponseData MyLoginException(MyCustomerLoginException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append("操作失败:");
        strBuilder.append(e.getMessage());
        map.put(Constant.JSON_MESSAGE, strBuilder.toString());
        map.put(Constant.JSON_CODE, JsonCode.LOGIN.getValue());//返回错误信息
        log.info("未登录", e.getMessage());
        return map;
    }*/


    /**
     * web的restful响应异常
     * 错误信息提示（例子）：restful请求不符合接口规范：服务端支持的接口类型是POST但传递的类型是GET
     */
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseData requestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        String methord=e.getMethod();//前端请求的restful类型，如put、post等
        String[] supportedMethods=e.getSupportedMethods();//获取服务端支持的restful类型，如put、post等
        StringBuilder stringBuilder=new StringBuilder();
        stringBuilder.append("服务端支持的接口类型是");
        for (int i = 0; i < supportedMethods.length; i++) {
            String supportedMethod=supportedMethods[i];
            stringBuilder.append(supportedMethod);
            if(i<supportedMethods.length-1){
                stringBuilder.append("|");
            }
        }
        stringBuilder.append("但传递的类型是"+methord);
        String error=stringBuilder.toString();
        //未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
        log.info("restful请求不符合接口规范", error);
        return ResponseData.error(ResultCode.METHODNOTALLOW.getCode(),"restful请求不符合接口规范："+ error);
    }

    /**
     * spring mvc在接收参数时，如果类型转换错误则会弹出此异常
     * 例如：某控制器的某个请求如下
     * public ResponseData hello(Integer number1, BigDecimal number2){
     * }
     * 如果传递过来的number1为a，则会出现下述异常
     */
    @ExceptionHandler
    public ResponseData methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        String fieldName=e.getName();//出错的字段名
        Object fieldValue=e.getValue();//出错字段的值
        Class type=e.getRequiredType();//出错字段的类型
        String error="类型转换错误：fieldName的值为fieldValue，无法转换为type类型"
                .replace("fieldName",fieldName)
                .replace("fieldValue",fieldValue.toString())
                .replace("type",type.getName());
        log.info("spring mvc参数类型转换异常", error);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),error);
    }

    /**
     * json转换异常时出现
     * 例1：
     * @PostMapping("/testPost")
     * public ResponseData jsonTest(@Valid @RequestBody User user){
     * 如果没有传递user对象，则会出现该异常
     */
    @ExceptionHandler
    public ResponseData httpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.info("json转换异常", e.getMessage());
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),"json转换异常："+ e.getMessage());
    }


    /**
     * @author ljy
     * 20220115
     * spring自定义验证框架异常
     * 说明：本异常只针对@valid修饰的(但是不包括和@)对象中属性验证的异常
     * 例1：典型触发该异常的函数
     * @GetMapping
     * public ResponseData hello(Integer number1, @Valid User user)
     * 并且在User类在中设置了
     * public class User implements java.io.Serializable {
     *     @NotBlank(message = "账户名不能为空")
     *     private String username;
     * 如果user对象中的username（账户名）为空，那么就会出现该异常
     * 例2：用@Valid @RequestBody同时修饰的参数不会出现该异常，而是ConstraintViolationException异常
     * @PostMapping("/testPost")
     * public ResponseData jsonTest(@Valid @RequestBody User user){
     */
    @ExceptionHandler
    public ResponseData bindException(BindException e) {
        //获取该异常的错误信息返回对象
        BindingResult bindingResult=e.getBindingResult();
        StringBuilder strBuilder = new StringBuilder();
        String className=bindingResult.getObjectName();//出错的参数，按例子来说这里是user
        strBuilder.append(className+"参数验证不通过，错误原因如下:");
        List<ObjectError> allErrors = bindingResult.getAllErrors();//获取具体的错误信息，这里会获取user对象中错误参数信息集合
        for(int i=0;i<allErrors.size();i++){
            ObjectError error=allErrors.get(i);
            if(i>0){
                strBuilder.append(",");
            }
            strBuilder.append(error.getDefaultMessage());//错误信息
        }
        String errorResult=strBuilder.toString();
        log.info("spring mvc参数绑定异常", errorResult);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),errorResult);
    }

    /**
     * @author ljy
     * 20220115
     * 处理spring 校验框架validation抛出的异常(二种异常之一)
     * 说明：本异常一般是针对@NotBlank或@NotNull等直接捕获的异常，不包括@valid单独修饰对象时的属性验证异常
     * 或者说使用了spring boot验证框架的异常如果不是BindException，基本都会触发该异常
     * 例如：
     * 在业务层接口编写了验证方法
     *  boolean login(@Valid User user, @NotBlank(message = "账户名不能为空3") String username, String password);
     *  那么当username验证不通过时，会出现ConstraintViolationException异常
     *  反之，如果是user出现异常，则不会出现ConstraintViolationException异常，而是BindException异常
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseData constraintViolationException(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> violations = new LinkedHashSet(e.getConstraintViolations());
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append("操作失败：");
        int index=0;//用于set集合中的计数，方便更好的处理输出信息格式
        for (ConstraintViolation<?> violation : violations) {//如果配置hibernate验证（fail_fast）后，这里就只会出现一个异常了，不过暂时保留此处
            if(index>0){
                strBuilder.append(",");
            }
            String msg=violation.getMessage();//出错的信息
            strBuilder.append(msg);
            index++;
            //break;//因为所有验证在前端显示，格式需要假如html和css进行控制，如果前端包括android等不好控制，所以只显示一条
        }
        String result = strBuilder.toString();
        log.info("spring业务层验证异常", result);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),result);
    }

    /**
     * 20220115：目前未发现该异常出现
     * 20220414：该异常在spring mvc层出现
     * 例如：下述情况会发生该异常
     * public ResponseData updateEnabled(@Valid @RequestBody RoleEnabledParam roleEnabledParam){
     * 并且针对RoleEnabledParam属性做了字段验证
     * public class RoleEnabledParam {
     *     @NotEmpty(message = "ids不能为空")
     *     private Integer[] ids; // 角色id集合
     *     @NotNull(message = "enabled不能为空")
     *     private Boolean enabled; // 可用属性
     * }
     * 处理spring 校验框架validation抛出的异常(二种异常之一)：MethodArgumentNotValidException（官方定义），该异常在控制器验证时出现
     */
    @ExceptionHandler(MethodArgumentNotValidException .class)
    public ResponseData methodArgumentNotValidException (MethodArgumentNotValidException e) {
        // 同样是获取BindingResult对象，然后获取其中的错误信息
        // //在配置hibernate验证（fail_fast）后，这里就只会出现一个异常了
        //如果没有，则可能又多个
        BindingResult bindingResult=e.getBindingResult();//如果配置在spring mvc的验证，会出现这个异常信息
        List<ObjectError> errorList=bindingResult.getAllErrors();//获取异常集合
        StringBuilder messageString=new StringBuilder();
        List<String> messageList=new ArrayList<>();
        for(int index=0;index<errorList.size();index++){
            ObjectError error=errorList.get(index);
            if(index!=0){
                messageString.append(";");
            }
            messageList.add(error.getDefaultMessage());
            messageString.append(error.getDefaultMessage());
        }
//        ObjectError error=errorList.get(0);//由于配置里只有一个异常，所以直接取第一个
//        String message=error.getDefaultMessage();//验证失败的错误信息
        String message="验证失败：message".replace("message",messageString.toString());
        log.info("spring的web验证异常", message);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),message,messageList);
    }

    //spring mvc在接收参数时，如果无法接收到值出现此异常（如该值是必须的，或是不符合传输类型要求，导致无法接收到值）
    @ExceptionHandler
    public ResponseData missingServletRequestParameterException(MissingServletRequestParameterException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        String fieldName=e.getParameterName();//出错的字段名
        String type=e.getParameterType();//出错字段的类型
        String error="参数类型不匹配：参数fieldName类型应该为type"
                .replace("fieldName",fieldName)
                .replace("type",type);
        log.info("spring的request参数接收不到", error);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),error);
    }


    //spring的文件上传大小异常
    /*@ExceptionHandler(MultipartException.class)
    public Map<String, Object> multipartException(MultipartException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        map.put("state", "上传文件超出大小");  //专门为ueditor写的返回信息，如果不需要可以去掉该行
        map.put(Constant.JSON_MESSAGE, "上传文件超出大小");
        map.put(Constant.JSON_CODE, JsonCode.ERROR.getValue());//返回错误信息
        //未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
        log.info("spring上传组件异常", "上传文件超出大小");
        return map;
    }*/


    //数字转换异常
   /* @ExceptionHandler(NumberFormatException.class)
    public Map<String, Object> numberFormatException(NumberFormatException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        map.put(Constant.JSON_MESSAGE, "类型转换错误：无法将指定字符串转化为数字");
        map.put(Constant.JSON_CODE, JsonCode.ERROR.getValue());//返回错误信息
        //未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
        log.info("字符串无法转换为数字的异常", "类型转换错误：无法将指定字符串转化为数字");
        return map;
    }*/




    /**
     * 处理其他异常
     *  未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public ResponseData unknownException(Exception e, HttpServletRequest request) {
        if (request!=null) {
            StringBuilder strBuild = new StringBuilder();
            strBuild.append("global catch reqUrl:");
            strBuild.append(request.getRequestURI());
            if (HttpMethod.GET.matches(request.getMethod())) {
                strBuild.append(" queryString:");
                strBuild.append(request.getQueryString());
            }
            log.error(strBuild.toString(), e);
            return ResponseData.error(ResultCode.INTERNALSERVERERROR.getCode(), ResultCode.INTERNALSERVERERROR.getMsg()+e.getMessage());
        }else{
            log.error("global catch", e);
            return ResponseData.error(ResultCode.INTERNALSERVERERROR.getCode(), ResultCode.INTERNALSERVERERROR.getMsg()+e.getMessage());
        }
    }
}
